//Brian Barton

import java.net.*;
import java.io.*;
import java.util.Calendar;
import java.util.concurrent.*;

public class ThreadedServer {
	public static void main (String args[]) throws IOException {
		int clientPort = 0;
		int servicePort = 0;
		
		if (args.length > 0) {
			clientPort = Integer.parseInt(args[0]);
		}
		
		ServerSocket ssClient = null;
		ServerSocket ssService = null;
		Socket s = null;
		InputStream is = null;
		BufferedInputStream bis = null;			
		DataInputStream dis = null;
		
		//create stats instance to save your stats
		Stats stats = new Stats();
		
		try {
			ssClient = new ServerSocket(clientPort);
			clientPort = ssClient.getLocalPort();
			
			servicePort = clientPort + 1;
			ssService = new ServerSocket(servicePort);
			
			InetAddress localHost = InetAddress.getLocalHost();
			
			System.out.println("Client Port: " + Integer.toString(clientPort));
			System.out.println("Service Port: " + Integer.toString(servicePort));
			LogMessage.say("Host: " + localHost.toString());
			
			//create thread to service clients
			ClientThread ct = new ClientThread(stats, ssClient);
			ct.setName("Client-Thread");
			ct.start();
			
			//then start the admin spin
			while (true) {
				s = ssService.accept();
				s.setSoTimeout(1500);
				
				is = s.getInputStream();
				bis = new BufferedInputStream(is, 4096);
				dis = new DataInputStream(bis);
				
				String command = "";
				char c = dis.readChar();
				while(c != '\0') {
					command += c;
					c = dis.readChar();
				}
					
				if (command.equals("stats")) {
					stats.printStatistics();
				}
					
				else if (command.equals("shutdown")) {
					LogMessage.say("Shutdown request received.");
					ct.interrupt();
					ct.join(); //wait for client thread to finish before exiting
					System.exit(0);		
				}
			}			
		} catch (IOException e) {
			e.printStackTrace();
		} catch (InterruptedException e) {
			LogMessage.say("join interrupted");
		} finally {
			try {
				if (dis != null) {
					dis.close();
				}
				
				if (bis != null) {
					bis.close();
				}
				
				if (is != null) {
					is.close();
				}
				
				if (s != null) {
					s.close();
				}
				
				if (ssService != null) {
					ssService.close();
				}
				
				if (ssClient != null) {
					ssClient.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}
		
class ClientThread extends Thread {	
	private Stats stat;
	private ServerSocket ss;
	private ThreadPoolExecutor tpe;
	
	public ClientThread(Stats stat, ServerSocket ss) {
		this.stat = stat;
		this.ss = ss;
		tpe = new ThreadPoolExecutor(3, 3, 15,
				TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(5));
	}
	
	public void run() {		
		Socket s = null;
		
		try {
			while (!this.isInterrupted()) {
				LogMessage.say("Waiting for connection...");
				s = ss.accept();
				stat.logConnect();
				s.setSoTimeout(1500);
				
				InetAddress ip = s.getInetAddress();
				LogMessage.say("Connection from: " + ip.toString()
								+ " " + Integer.toString(s.getPort()));
				//announces connection ip and port
				
				//create a new task and send them to the executor
				Submit sub = new Submit(s, stat);
				tpe.execute(sub);
			}
			
			if (ClientThread.interrupted()) {
				tpe.shutdown();
				try {
					tpe.awaitTermination(15, TimeUnit.SECONDS);
				} catch (InterruptedException e) {
					LogMessage.say("Thread pool interrupted while terminating!");
				}
				stat.printStatistics();
				this.interrupt(); //resets interrupt flag
			}
		} catch (IOException e) {
			if (ClientThread.interrupted()) {
				if (!tpe.isShutdown()) {
					tpe.shutdown();
				}
				try {
					tpe.awaitTermination(15, TimeUnit.SECONDS);
				} catch (InterruptedException e1) {
					LogMessage.say("Thread pool interrupted during shutdown!");
				}
				stat.printStatistics();
				this.interrupt(); //resets interrupt flag
			}
			
			else {
				e.printStackTrace();
			}
		} catch (RejectedExecutionException re) {
			LogMessage.say("Thread pool shutting down, execution rejected");
		}
		finally {
			try {
				if (s != null) {
					s.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	public void interrupt() {
		super.interrupt(); //sets interrupt flag
		
		try {
			if (ss != null) {
				ss.close(); //this will cause an IOException if blocked on accept
			}		
		} catch (IOException e) {
			System.out.println("all of your interrupt exceptions are belong to us");
		}
	}
}

class Submit extends Thread {
	private Socket s;
	Stats stats;
	
	public Submit(Socket sock, Stats stats) {
		this.s = sock;
		this.stats = stats;
	}
	
	public void run() {
		InputStream is = null;
		BufferedInputStream bis = null;
		DataInputStream dis = null;
		OutputStream os = null;
		BufferedOutputStream bos = null;
		DataOutputStream dos = null;
		FileOutputStream fos = null;
		
		try {
			InetAddress ip = s.getInetAddress();
			System.out.println("### Connection from: "
					+ ip.toString() + " " + Integer.toString(s.getPort()));
			//announces connection ip and port
		
			is = s.getInputStream();
			bis = new BufferedInputStream(is);
			dis = new DataInputStream(bis);
			
			//first get the username
			String username = "";
			char c = dis.readChar();
			while(c != '\0') {
				username += c;
				c = dis.readChar();
			}
			System.out.println("### Getting connection from user: " + username);
		
			//next get the number of files
			int numFiles = dis.readInt();
			System.out.println("### Expecting "
				+ Integer.toString(numFiles) + " files");
		
			//next create a directory to store the files
			Calendar now = Calendar.getInstance();
			String directory = username + "."
					+ String.format("%02d", now.get(Calendar.MONTH)+1) 
					+ String.format("%02d", now.get(Calendar.DAY_OF_MONTH))
					+ String.format("%02d", now.get(Calendar.HOUR_OF_DAY))
					+ String.format("%02d", now.get(Calendar.MINUTE))
					+ String.format("%02d", now.get(Calendar.SECOND));
			
			File dir = new File(directory);
			if (!dir.mkdir()) throw new IOException();
			//if (Debug) System.out.println("### Created directory " + directory);
			
			//then for each file get
			//1. the name of the file
			//2. the size of the file (in bytes)
			//3. the bytes of the file
			File f = null;
			byte[] b = null;
			int bytesRead = 0;
			int totalBytesRead = 0;
			long fileSize = 0;
			boolean allRec = true;
			
			String filename = "";
			c = '\0';
			
			for (int i = 0; i < numFiles; i++) {
				filename = "";
				totalBytesRead = 0;
				b = new byte[512];
				
				c = dis.readChar();
				
				while (c != '\0') {
					filename += c;
					c = dis.readChar();
				}	
				
				LogMessage.say("Receiving file name: " + filename);
			
				fileSize = dis.readLong();
				LogMessage.say("File is " + fileSize + " bytes long");
				
				String pathname = directory + "/" + filename;
				f = new File(pathname);
				f.createNewFile();
				fos = new FileOutputStream(f);
				
				while(totalBytesRead < fileSize) {
					if ((fileSize - totalBytesRead) < b.length) {
						bytesRead = dis.read(b, 0, (int) fileSize - totalBytesRead);
					}	
				
					else {
						bytesRead = dis.read(b);
					}	
				
					if (bytesRead == -1) {
						LogMessage.say("Error receiving file: " + filename);
						allRec = false;
					}	
				
					fos.write(b, 0, bytesRead);
					totalBytesRead += bytesRead;
				}	
			
				if (allRec) {
					LogMessage.say("Received file " + filename);
				}
			}
		
			if (allRec) {
				synchronized (stats) {
					File done = new File(directory + '/' + "SUBMIT_DONE");
					done.createNewFile();
					stats.logSubmission(numFiles, username);
				}
				
				LogMessage.say("Wrote SUBMIT_DONE");
		
				os = s.getOutputStream();
				bos = new BufferedOutputStream(os);
				dos = new DataOutputStream(bos);
				dos.write(1); //sends true value
				dos.flush();
				LogMessage.say("Sent boolean response: true");
			}	
		} catch (IOException e) {
			e.printStackTrace();
		}	finally {
			try {
				if (dos != null) {
					dos.close();
				}
	
				if (os != null) {
					os.close();
				}
	
				if (fos != null) {
					fos.close();
				}
	
				if (dis != null) {
					dis.close();
				}
	
				if (bis != null) {
					bis.close();
				}
	
				if (is != null) {
					is.close();
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

class Stats {
	private int numConnections;
	private int numFiles;
	private String lastUser;
	
	public Stats() {
		numConnections = 0;
		numFiles = 0;
		lastUser = null;
	}
	
	public synchronized void logConnect() {
		numConnections++;
	}
	
	public synchronized void logSubmission(int numFiles, String user) {
		this.numFiles += numFiles;
		lastUser = user;
	}
	
	public synchronized void printStatistics() {
		String out = "Printing statistics\n" +
				"Connections: " + numConnections +
				"\nFiles: " + numFiles;
		
		if (lastUser != null) {
			out = out + "\nLast User: " + lastUser;
		}
		
		else {
			out = out + "\nNo successful submissions";
		}
		
		System.out.println(out);
	}
}

class LogMessage {
	public static void say(String s) {
		Thread t = Thread.currentThread();
		s = "### " + t.getName() + ": " + s;
		System.out.println(s);
	}
}